/**
 * Title: TokenServiceImpl.java
 * Description: Copyright: Copyright (c) 2019 Company: BHFAE
 *
 * @author Sharp
 * @date 2019-7-26 18:05
 * @description Project Name: Grote
 * @Package: com.srct.service.account.service.impl
 */
package com.sharp.authorization.service.token.impl;

import com.sharp.authorization.constants.param.AccountParam;
import com.sharp.authorization.constants.token.TokenItemConst;
import com.sharp.authorization.constants.token.TokenTypeConst;
import com.sharp.authorization.holder.TokenHolder;
import com.sharp.authorization.service.token.TokenService;
import com.sharp.kernel.constant.err.ErrCodeSys;
import com.sharp.framework.service.cache.FrameCacheService;
import com.sharp.framework.service.redis.RedisService;
import com.sharp.kernel.utils.DateUtil;
import com.sharp.kernel.utils.JsonUtil;
import com.sharp.framework.utils.security.RandomUtil;
import com.sharp.kernel.validate.Validator;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.MapUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.util.*;

@Slf4j
@Service
public class TokenServiceImpl implements TokenService {

    private static final String BASE_KEY = "Token:";
    private static final String SEPARATION = ":";
    private static final String TIMESTAMP = "TIMESTAMP";
    private static final String EXPIRED = "EXPIRED";
    private static final String EXPIRED_PARAM_SUFFIX = "_EXPIRED";
    private static final int TOKEN_DEFAULT_TIMEOUT = 1200;

    @Resource
    private RedisService redisService;
    @Resource
    private FrameCacheService frameCacheService;

    @Override
    public String fetchToken() {
        String token = BASE_KEY + RandomUtil.getUUID();
        saveToken("token", token, TOKEN_DEFAULT_TIMEOUT);
        return token;
    }

    private void saveToken(String tokenType, String token, int expired) {
        Map<String, Object> tokenMap = new HashMap<>(16);
        tokenMap.put(tokenType, token);
        tokenMap.put(TIMESTAMP, DateUtil.formatDate(new Date(), DateUtil.DATE_TIME_NORMAL));
        tokenMap.put(EXPIRED, expired);
        String key = getKey(token);
        redisService.hashSet(key, tokenMap);
        redisService.expire(key, expired);
    }

    private String getKey(String token) {
        return BASE_KEY + token;
    }

    @Override
    public boolean verifyToken(String token, String userId) {
        Validator.assertTrue(StringUtils.isNotBlank(token), ErrCodeSys.SYS_SESSION_TIME_OUT);
        boolean result = redisService.hasKey(getKey(token));
        if (result) {
            String operator = getItem(token, TokenItemConst.OPERATOR.name(), String.class);
            result = StringUtils.equals(userId, operator);
        }
        return result;
    }

    @Override
    public <T> T getItem(String token, String itemKey, Class<?> collectionClass, Class<?>... elementClasses) {
        Map<String, Object> map = getTokenValue(token);
        Validator.assertNotEmpty(map, ErrCodeSys.SYS_SESSION_TIME_OUT);
        Validator.assertNotEmpty(map, ErrCodeSys.PA_DATA_NOT_EXIST, "token内容");
        return JsonUtil.readJson(JsonUtil.toJson(map.get(itemKey)), collectionClass, elementClasses);
    }

    @Override
    public Map<String, Object> getTokenValue(String token) {
        return redisService.hashGet(getKey(token));
    }

    @Override
    public String buildAppAccessToken(String customerNumber) {
        boolean loginSingleton = frameCacheService.getParamSwitch(AccountParam.LOGIN_SINGLETON);
        if (loginSingleton) {
            removeTokenForLoginSingleton(customerNumber, TokenTypeConst.APP_ACCESS_TOKEN);
        }
        return buildToken(customerNumber, TokenTypeConst.APP_ACCESS_TOKEN);
    }

    private void removeTokenForLoginSingleton(String customerNumber, TokenTypeConst tokenType) {
        String prefixKeys = BASE_KEY + customerNumber + SEPARATION + tokenType.toString() + SEPARATION;
        Set<String> tokenSet = redisService.keys(prefixKeys);
        log.info("用户:{}, 类型:{}, 已登录数: {} ", customerNumber, tokenType.name(), tokenSet.size());
        if (CollectionUtils.isNotEmpty(tokenSet)) {
            redisService.delete(new ArrayList<>(tokenSet));
        }
    }

    private String buildToken(String customerNumber, TokenTypeConst tokenType) {
        String token = customerNumber + SEPARATION + tokenType.toString() + SEPARATION + RandomUtil.getUUID();
        AccountParam expiredParam = getExpiredParamByTokenType(tokenType.toString());
        int expired = frameCacheService.getParamInt(expiredParam);
        saveToken(tokenType.name(), token, expired);
        return token;
    }

    private AccountParam getExpiredParamByTokenType(String tokenType) {
        return AccountParam.valueOf(tokenType + EXPIRED_PARAM_SUFFIX);
    }

    @Override
    public String buildAppRefreshToken(String customerNumber) {
        boolean loginSingleton = frameCacheService.getParamSwitch(AccountParam.LOGIN_SINGLETON);
        if (loginSingleton) {
            removeTokenForLoginSingleton(customerNumber, TokenTypeConst.APP_REFRESH_TOKEN);
        }
        return buildToken(customerNumber, TokenTypeConst.APP_REFRESH_TOKEN);
    }

    @Override
    public String buildWebAccessToken(String customerNumber) {
        boolean loginSingleton = frameCacheService.getParamSwitch(AccountParam.LOGIN_SINGLETON);
        if (loginSingleton) {
            removeTokenForLoginSingleton(customerNumber, TokenTypeConst.WEB_ACCESS_TOKEN);
        }
        return buildToken(customerNumber, TokenTypeConst.WEB_ACCESS_TOKEN);
    }

    @Override
    public String buildWebRefreshToken(String customerNumber) {
        boolean loginSingleton = frameCacheService.getParamSwitch(AccountParam.LOGIN_SINGLETON);
        if (loginSingleton) {
            removeTokenForLoginSingleton(customerNumber, TokenTypeConst.WEB_REFRESH_TOKEN);
        }
        return buildToken(customerNumber, TokenTypeConst.WEB_REFRESH_TOKEN);
    }

    @Override
    public String buildWechatToken(String customerNumber) {
        boolean loginSingleton = frameCacheService.getParamSwitch(AccountParam.LOGIN_SINGLETON);
        if (loginSingleton) {
            removeTokenForLoginSingleton(customerNumber, TokenTypeConst.WECHAT_TOKEN);
        }
        return buildToken(customerNumber, TokenTypeConst.WECHAT_TOKEN);
    }

    @Override
    public String buildIotToken(String customerNumber) {
        return buildToken(customerNumber, TokenTypeConst.IOT_TOKEN);
    }

    @Override
    public String buildEmailToken(String userId) {
        return buildToken(userId, TokenTypeConst.CHANGE_PWD_TOKEN);
    }

    @Override
    public String buildOpenPlatformToken(String appKey) {
        return buildToken(appKey, TokenTypeConst.PLATFORM_TOKEN);
    }

    @Override
    public Map<String, Object> getTokenValue() {
        return getTokenValue(getToken());
    }

    @Override
    public String getToken() {
        return getAndValidateToken();
    }

    @Override
    public void setToken(String token) {
        validateAndSetToken(token);
    }

    private void validateAndSetToken(String token) {
        Validator.assertNotBlank(token, ErrCodeSys.PA_DATA_NOT_EXIST, "token");
        TokenHolder.set(token);
    }

    private String getAndValidateToken() {
        String token = TokenHolder.get();
        Validator.assertNotBlank(token, ErrCodeSys.SYS_SESSION_TIME_OUT);
        return token;
    }

    @Override
    public void setTokenValue(Map<String, Object> map) {
        setTokenValue(getToken(), map);
    }

    @Override
    public void setTokenValue(String token, Map<String, Object> map) {
        int expired = Integer.parseInt(map.getOrDefault(EXPIRED, TOKEN_DEFAULT_TIMEOUT).toString());
        String key = getKey(token);
        map.put(TIMESTAMP, DateUtil.formatDate(new Date(), DateUtil.DATE_TIME_NORMAL));
        redisService.hashSet(key, map);
        redisService.expire(key, expired);
    }

    @Override
    public boolean isTokenExist() {
        return isTokenExist(getToken());
    }

    @Override
    public boolean isTokenExist(String token) {
        return redisService.hasKey(getKey(token));
    }

    @Override
    public void putItem(String itemKey, Object value) {
        putItem(getToken(), itemKey, value);
    }

    @Override
    public void putItem(String token, String itemKey, Object value) {
        Map<String, Object> map = getTokenValue(token);
        if (MapUtils.isEmpty(map)) {
            map = new HashMap<>(16);
        }
        map.put(itemKey, value);
        setTokenValue(token, map);
    }

    @Override
    public void removeItem(String... itemKey) {
        String token = getAndValidateToken();
        removeItemByToken(token, itemKey);
    }

    @Override
    public void removeItemByToken(String token, String... itemKey) {
        redisService.hashDel(getKey(token), itemKey);
    }

    @Override
    public void removeToken() {
        removeToken(getToken());
    }

    @Override
    public void removeToken(String token) {
        redisService.delete(getKey(token));
    }

    @Override
    public String getTokenType() {
        return getTokenType(getToken());
    }

    @Override
    public String getTokenType(String token) {
        return token.split(SEPARATION)[1];
    }

    @Override
    public String getCurrentUserId() {
        String userId = getItem(TokenItemConst.OPERATOR.name(), String.class);
        Validator.assertNotBlank(userId, ErrCodeSys.PA_DATA_NOT_EXIST, "用户id");
        return userId;
    }

    @Override
    public <T> T getItem(String itemKey, Class<?> collectionClass, Class<?>... elementClasses) {
        return getItem(getToken(), itemKey, collectionClass, elementClasses);
    }

    @Override
    public void updateTokenValue(String token, String key, Object obj) {
        String tokenKey = getKey(token);
        Map<String, Object> tokenMap = redisService.hashGet(tokenKey);
        Validator.assertNotNull(tokenMap, ErrCodeSys.SYS_SESSION_TIME_OUT);
        tokenMap.put(key, obj);
        int expired = Integer.parseInt(tokenMap.getOrDefault(EXPIRED, TOKEN_DEFAULT_TIMEOUT).toString());
        tokenMap.put(TIMESTAMP, DateUtil.formatDate(new Date(), DateUtil.DATE_TIME_NORMAL));
        redisService.hashSet(tokenKey, tokenMap);
        redisService.expire(tokenKey, expired);
    }
}
